/*
* Copyright (c) 2007, Nubo Technologies
* 
* All rights reserved.
* 
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
*
*    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
*    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
*    * Neither the name of the Nubo Technologies nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package com.nubotech.gwt.oss.client.auth;

import com.google.gwt.core.client.GWT;
//import com.google.gwt.gears.client.Factory;
//import com.google.gwt.gears.client.GearsException;
//import com.google.gwt.gears.client.database.Database;
//import com.google.gwt.gears.client.database.DatabaseException;
//import com.google.gwt.gears.client.database.ResultSet;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.json.client.JSONNumber;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.client.Cookies;
import com.google.gwt.user.client.Window;
import com.nubotech.gwt.oss.client.AccessControlPolicy;
import com.nubotech.gwt.oss.client.Bucket;
import com.nubotech.gwt.oss.client.ObjectHolder;
import com.nubotech.gwt.oss.client.OnlineStorageService;
import com.nubotech.gwt.oss.client.OnlineStorageServiceFactory;
import com.nubotech.gwt.oss.client.util.AesCipher;
import com.nubotech.gwt.oss.client.util.LogUtil;
import com.nubotech.gwt.oss.client.util.Sha1;

/**
 * 
 * manager used to store and get credentials. Credentials can be stored locally using Google Gears or remotely.
 * @author jonnakkerud
 */
public class AuthenticationManager { 
    
    public enum Storage {NONE, LOCAL, REMOTE}
    
    private static AuthenticationManager instance = new AuthenticationManager();
    
    //private Database db;
    private Credential login;    
    private String prefix = "oss";
    
    private AuthenticationManager() {
    }
    
    public static AuthenticationManager instance() {
        return instance;
    }

    public void update(String email, String pass, String account, String key) {
        Credential c = new Credential();
        c.setEmail(email);
        c.setPass(pass);
        c.setAccount(account);
        c.setSecretKey(key);
        update(email, pass, account, key, Storage.NONE);
    }

     public void update(String email, String pass, String account, String key, Storage storageType) {
        Credential c = new Credential();
        c.setEmail(email);
        c.setPass(pass);
        c.setAccount(account);
        c.setSecretKey(key);
        update(c, storageType);
    }
   
    public void update(Credential c, Storage storageType) {
        switch(storageType) {
            case NONE: updateImpl(c); break;
            //case LOCAL: updateLocalImpl(c); break;
            case REMOTE: updateRemoteImpl(c); break;
        }        
    }
    
    /*private void processCredentialLocal(String email, String pass, AuthenticationListener listener) {
        Credential c = null;
        if (isGearsAvailable()) {
            opendb();
            try {
                ResultSet rs = db.execute("select email, pass, account, skey from Credential where email = ?", new String[] {email});
                if (rs.isValidRow()) {
                    c = new Credential();
                    c.setEmail(rs.getFieldAsString(0));
                    c.setPass(rs.getFieldAsString(1));
                    c.setAccount(rs.getFieldAsString(2));
                    c.setSecretKey(decrypt(c.createKey(pass), rs.getFieldAsString(3)));
                }
                rs.close();
            } catch (DatabaseException e) {
                listener.loginFail("Unable to get Credential: " + e.getMessage());
            } finally {
                closedb();
            }
        }
        
        try {
            validateLogin(email, pass, c);
        }
        catch(AuthenticationException ex) {
            listener.loginFail(ex.getMessage());
            return;
        }
        listener.loginSuccess(getLogin());
    }*/
    
    public Credential getLogin() {
        return login;
    }
    
    public boolean isLogin() {
        return (login != null);
    }
    
    public void login(String email, String pass, AuthenticationListener listener) {
        login(email, pass, (credentialIsLocal() ? Storage.LOCAL : Storage.REMOTE), listener);
    }
        
    public void login(String email, String pass, Storage storageType, AuthenticationListener listener) {      
        Credential c = getLogin();
        if (c != null) {
            // already logged in, just fire success and exit
            listener.loginSuccess(c);
            return;
        }
        
        switch(storageType) {
            case NONE: break;
            //case LOCAL: processCredentialLocal(email, pass, listener); break;
            case REMOTE: processCredentialRemote(email, pass, listener); break;
        }        
        
    }

     public void loginRemote(String email, String pass, String bucket, String credential, AuthenticationListener listener) {       
         Credential c = getLogin();
        if (c != null) {
            // already logged in, just fire success and exit
            listener.loginSuccess(c);
            return;
        }
        
        processCredentialRemote(email, pass, listener, bucket, credential);
    }
   
    
    public void logout() {
        login = null;
    }
    
    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }
        
    /*private void opendb() {
        try {
            if (db == null) {
                db = Factory.getInstance().createDatabase();
                db.open("awss3");
                db.execute("create table if not exists Credential (id INTEGER, email TEXT, pass TEXT, account TEXT, skey TEXT)");
            }
        } catch (GearsException e) {
            GWT.log("Unable to open DB awss3", e);
        }                
    }

    private void closedb() {
        try {
            if (db != null) {
                db.close();
                db = null;
            }
        } catch (GearsException e) {
            GWT.log("", e);
        }                        
    }*/
    
    private void updateImpl(Credential c) {
        this.login = c;
    }

    private void updateRemoteImpl(Credential c) {
        OnlineStorageService service = OnlineStorageServiceFactory.getService(c);
        
        // batch the calls
        service.beginBatch();

        // put remote bucket = prefix + sha1_hex(Location.host)
        String bucketName = prefix + "-" + Sha1.hex_sha1(Window.Location.getHostName());
                
        service.createBucket(bucketName);
        
        // put file name = sha1_hex(c.email + c.pass) + /prefix.credential : public read
        String keyName = Sha1.hex_sha1(c.getEmail()+c.getPass()) + "/" + getPrefix() + ".credentials";
        
        // file content = encrypt account + "," + secret (key = email + pass)
        String content = c.getAccount() + "," + c.getSecretKey();
        String key = c.getEmail() + c.getPass();
        
        ObjectHolder holder = new ObjectHolder();
        holder.setBucket(new Bucket(bucketName));
        holder.setKey(keyName);
        holder.setAccessPolicy(AccessControlPolicy.PUBLIC_READ);
        
        AesCipher cipher = new AesCipher(key);
        String iv = cipher.generateSharedKey();
        String cipherText = cipher.encrypt(content);
               
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        //a = content
        sb.append("\"a\":").append('"').append(cipherText).append('"').append(",");
        // b = iv
        sb.append("\"b\":").append('"').append(iv).append('"').append(",");
        // c = len
        sb.append("\"c\":").append(cipher.getOriginalSize());

        sb.append("}");

       
        holder.setData(sb.toString());
        //holder.addMetaData("iv", iv);
        //holder.addMetaData("len", String.valueOf(cipher.getOriginalSize()));
        service.putObject(holder, null);
        
        service.commitBatch();
        
        Cookies.setCookie("oss-storage", "2");
    }

    /*private void updateLocalImpl(Credential c) {
        opendb();
                
        boolean exists = hasLocalRecord("1");
        String statement = "insert into Credential values (?, ?, ?, ?, ?)";
        String[] args = new String[] {"1", c.getEmail(), c.createHashedPass(), c.getAccount(), encrypt(c.createKey(c.getPass()), c.getSecretKey())};
        if (exists) {
            statement = "update Credential set email = ?, pass = ?, account = ?, skey = ? where id = 1";
            args = new String[] {c.getEmail(), c.createHashedPass(), c.getAccount(), encrypt(c.createKey(c.getPass()), c.getSecretKey())};
        }
        
        try {
            db.execute(statement, args);
        } 
        catch (DatabaseException e) {
            Window.alert(e.getMessage());
        }
        finally {
            closedb();
        }
        
        Cookies.setCookie("oss-storage", "1");
    }

    private boolean hasLocalRecord(String id) {
        boolean result = false;
        try {
            ResultSet rs = db.execute("select id from Credential where id = " + id);
            result = rs.isValidRow();
            rs.close();
        } catch (DatabaseException e) {
            Window.alert(e.getMessage());
        }
        return result;
    }*/
    
    private String encrypt(String key, String clearText) {
        AesCipher cipher = new AesCipher(key, AesCipher.Mode.OFB);
        return cipher.encrypt(clearText);
    }
    
    private String decrypt(String key, String cipherText) {
        AesCipher cipher = new AesCipher(key, AesCipher.Mode.OFB);
        return cipher.decrypt(cipherText);
    }

    private void processCredentialRemote(final String email, final String pass, final AuthenticationListener listener) {
        processCredentialRemote(email, pass, listener, null, null);
    }
 
    private void processCredentialRemote(final String email, final String pass, final AuthenticationListener listener, String bucket, String credential) {
        Credential c = new Credential();
        c.setAnonymous(true);
        OnlineStorageService service = OnlineStorageServiceFactory.getService(c);
        
        String bucketName = null;
        if (bucket != null) {
            bucketName = bucket;
        }
        else {
           // get remote bucket = prefix + sha1_hex(Location.host)
            bucketName = getPrefix() + "-" + Sha1.hex_sha1(Window.Location.getHostName());
        }
        
        String credentialKey = null;
        if (credential != null) {
            credentialKey = credential;
        }
        else {       
            // if exists then get the file info : sha1_hex(email + pass) + /prefix.credential
            credentialKey = Sha1.hex_sha1(email+pass) + "/" + getPrefix() + ".credentials";
        }
                
        // get public credential.  store iv and len in data as json
        StringBuilder url = new StringBuilder();
        url.append("http://").append(service.getHost()).append("/").append(bucketName);
        url.append("/").append(credentialKey);
        RequestBuilder request = new RequestBuilder(RequestBuilder.GET, url.toString());
        request.setCallback(new RequestCallback() {
            public void onResponseReceived(Request req, Response res) {
                try {
                    Credential credential = createCredentialFromJson(email, pass, res.getText());
                    validateLogin(email, pass, credential);
                }
                catch(Exception ex) {
                    login = null;
                    listener.loginFail(ex.getMessage());
                }
                
                if (login != null) {
                    listener.loginSuccess(login);
                }
            }

            public void onError(Request req, Throwable th) {
                login = null;
                listener.loginFail(th.getMessage());
            }
        });
        
        try {
            request.send();
        } 
        catch (RequestException ex) {
            login = null;
            listener.loginFail(ex.getMessage());            
        }
        
    }    

    public Credential createCredentialFromJson(String email, String pass, String json) {
        JSONObject jo = (JSONObject) JSONParser.parse(json);
        JSONString content = (JSONString) jo.get("a");
        JSONString iv = (JSONString) jo.get("b");
        JSONNumber origLen = (JSONNumber) jo.get("c");
        String key = email + pass;
        AesCipher cipher = new AesCipher(key);
        cipher.setHexIv(iv.stringValue());
        cipher.setOriginalSize(new Double(origLen.doubleValue()).intValue());
        String s = cipher.decrypt(content.stringValue());
        String[] arS = s.split(",");
        Credential cred = new Credential();
        cred.setEmail(email);
        cred.setAccount(arS[0]);
        cred.setSecretKey(arS[1]);
        cred.setPass(Sha1.hex_sha1(cred.getAccount() + pass));
        return cred;
    }
        
    private void validateLogin(String email, String pass, Credential c) throws AuthenticationException {
        if (c == null) throw new AuthenticationException("Email or password not found");
        String hash = Sha1.hex_sha1(c.getAccount()+pass);
        if (hash.equals(c.getPass()) == false) {
            throw new AuthenticationException("Password is incorrect");
        }
        login = c;
    }

    private boolean credentialIsLocal() {
        boolean retval = true;
        String s = Cookies.getCookie("oss-storage");
        if (s != null && Integer.parseInt(s) != 1) {
            retval = false;
        }
        return retval;
    }        
}
